#!/usr/bin/python
# web service, so return only JSON (no HTML)
from flask import Flask, jsonify, request, url_for, make_response, render_template, send_file
import sqlite3
import interface_db as d
import os.path
import urlparse
import argparse
import textwrap
import functools    # need to wrap own decorators to comply with flask views
import zipfile
from io import BytesIO
try:
    from flask.ext.cors import CORS  # The typical way to import flask-cors
except ImportError:
    # Path hack allows examples to be run without installation.
    import os
    parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    os.sys.path.insert(0, parentdir)

    from flask_cors import CORS



app = Flask(__name__)
app.debug=False
if not app.debug:
    print("adding logger")
    import logging
    from logging import FileHandler
    from logging import Formatter
    data_dir = os.path.expanduser('~/benchtracker_data/')
    if not os.path.isdir(data_dir):
        os.makedirs(data_dir)
    file_handler = FileHandler(os.path.join(data_dir, 'log.txt'))
    file_handler.setFormatter(Formatter(
        '%(asctime)s %(levelname)s: %(message)s '
        '[in %(pathname)s:%(lineno)d'
    ))
    file_handler.setLevel(logging.WARNING)
    app.logger.addHandler(file_handler)

cors = CORS(app)
port = 5000
database = None
root_directory = None



def parse_args(ns=None):
    """parse arguments from command line and return as namespace object"""
    parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=textwrap.dedent("""\
            serve a central database with benchmark information
            
        Generated database:
            Database should be created by populate_db.py, with each task
            organized as a table. A task is a collection of related benchmarks
            that are commonly run together."""),
            usage="%(prog)s [OPTIONS]")

    parser.add_argument("-d", "--database",
            default="results.db",
            help="name of database to store results in; default: %(default)s")
    parser.add_argument("-r", "--root_directory",
            default="~/benchtracker_data/",
            help="name of the directory to store databases in; default: %(default)s")
    parser.add_argument("-p", "--port",
            default=5000,
            type=int,
            help="port number to listen on; default: %(default)s")
    params = parser.parse_args(namespace=ns)
    global database, port, root_directory
    root_directory = os.path.expanduser(params.root_directory)
    database = os.path.expanduser(params.database)
    port = params.port
    return params



def catch_operation_errors(func):
    @functools.wraps(func)
    def task_checker(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except IOError as e:
            return jsonify({'status': 'File does not exist! ({})'.format(e)})
        except IndexError:
            return jsonify({'status': 'Index out of bounds! (likely table index)'})
        except sqlite3.OperationalError as e:
            return jsonify({'status': 'Malformed request! ({})'.format(e)})
    return task_checker

# each URI should be a noun since it is a resource (vs a verb for most library functions)

@app.route('/')
@app.route('/tasks', methods=['GET'])
@catch_operation_errors
def get_tasks():
    database = parse_db()
    return jsonify({'tasks':d.list_tasks(database), 'database':database})

@app.route('/db', methods=['GET'])
def get_database():
    return jsonify({'database':database})

@app.route('/param', methods=['GET'])
@catch_operation_errors
def get_param_desc():
    database = parse_db()
    tasks = parse_tasks()
    param = request.args.get('p')
    mode = request.args.get('m', 'range')   # by default give ranges, overriden if param is text
    try:
        (param_type, param_val) = d.describe_param(param, mode, tasks, real_db(database))
    except ValueError as e:
        return jsonify({'status': 'Parameter value error: {}'.format(e)})
    return jsonify({'status': 'OK', 
    'database':database,
    'tasks':tasks, 
    'param': param,
    'type': param_type,
    'val': param_val})

@app.route('/params/', methods=['GET'])
@catch_operation_errors
def get_shared_params():
    database = parse_db()
    tasks = parse_tasks()
    params = d.describe_tasks(tasks, real_db(database))
    return jsonify({'status': 'OK', 'database':database, 'tasks':tasks, 'params': params})

@app.route('/data', methods=['GET'])
@catch_operation_errors
def get_filtered_data():
    (exception, payload) = parse_data()
    if exception:
        return payload
    else:
        (databaes, tasks, params, data) = payload
        return jsonify({'status': 'OK', 
            'database':database,
            'tasks':tasks, 
            'params':params,
            'data':data})



@app.route('/csv', methods=['GET'])
@catch_operation_errors
def get_csv_data():
    """Return a zipped archive of csv files for selected tasks"""
    (exception, payload) = parse_data()
    if exception:
        return payload
    else:
        (database, tasks, params, data) = payload
        memory_file = BytesIO()
        with zipfile.ZipFile(memory_file, 'a', zipfile.ZIP_DEFLATED) as zf:
            t = 0
            for csvf in d.export_data_csv(params, data):
                # characters the filesystem might complain about (/|) are replaced
                zf.writestr("benchmark_results/" + tasks[t].replace('/','.').replace('|','__'), csvf.getvalue())
                t += 1

        # prepare to send over network
        memory_file.seek(0)
        return send_file(memory_file, attachment_filename="benchmark_results.zip", as_attachment=True)


@app.route('/view')
@catch_operation_errors
def get_view():
    database = parse_db()
    print(real_db(database))
    tasks = {task_name for (task_name) in d.list_tasks(real_db(database))}
    queried_tasks = parse_tasks()
    x = y = filters = ""
    # only pass other argument values if valid tasks selected
    if queried_tasks:
        x = request.args.get('x')
        y = request.args.get('y') 
        (temp, filters) = parse_filters(verbose=True)
        

    return render_template('viewer.html',
                            database=database,
                            tasks=tasks,
                            queried_tasks=queried_tasks,
                            queried_x = x,
                            queried_y = y,
                            filters = filters)

@app.errorhandler(404)
def not_found(error):
    resp = jsonify({'error': 'Not found'})
    resp.status_code = 404
    return resp

# library call knows path to database, web viewer doesn't for security reasons
def real_db(relative_db):
    return os.path.join(root_directory, relative_db)

# should always be run before all other querying since it determines where to look from
def parse_db():
    db = request.args.get('db')
    if db:
        return db
    # default to the global database
    return database

def parse_tasks():
    tasks = request.args.getlist('t')
    if tasks and tasks[0].isdigit():
        all_tasks = d.list_tasks(database)
        tasks = [all_tasks[int(t)] for t in tasks]
    return tasks

def parse_filters(verbose=False):
    """
    Parse filter from current request query string and return the filtered parameters and filters in a list

    verbose mode returns filters without splitting out the type
    """
    filter_param = None
    filter_method = None
    filters = []
    filter_args = []
    filter_params = []
    for arg in urlparse.parse_qsl(request.query_string):
        if arg[0][0] != 'f':
            continue
        # new filter parameter
        if arg[0] == 'fp':
            # previous filter ready to be built
            if filter_param and filter_method and filter_args:
                filters.append(d.Task_filter(filter_param, filter_method, filter_args))
                filter_args = []    # clear arguments; important!
                print("{}: {}".format(filters[-1], filters[-1].args))
                filter_params.append(filter_param)
            # split out the optional type following parameter name
            if verbose:
                filter_param = arg[1]
            else:
                filter_param = arg[1].split()[0]

        if arg[0] == 'fm':
            filter_method = arg[1]
        if arg[0] == 'fa':
            filter_args.append(arg[1])
    # last filter to be added
    if (not filters or filter_param != filters[-1].param) and filter_args:
        filters.append(d.Task_filter(filter_param, filter_method, filter_args))
        print("{}: {}".format(filters[-1], filters[-1].args))
        filter_params.append(filter_param)

    return filter_params,filters

def parse_data():
    """
    Parses request for data and returns a 2-tuple payload.

    First item is True for an exception occurance, with the 2nd item being the json response for error.
    Else false for no exception occruance, with second item being a 3-tuple.
    """
    database = parse_db()
    # split to get name only in case type is also given
    x_param = request.args.get('x')
    y_param = request.args.get('y')

    if not x_param:
        return (True, jsonify({'status': 'Missing x parameter!'})) 
    if not y_param:
        return (True, jsonify({'status': 'Missing y parameter!'})) 
    x_param = x_param.split()[0]
    y_param = y_param.split()[0]

    try:
        (filtered_params, filters) = parse_filters()
    except IndexError:
        return (True, jsonify({'status': 'Incomplete filter arguments!'}))
    except ValueError:
        return (True, jsonify({'status': 'Unsupported filter method!'}))

    tasks = parse_tasks()

    (params, data) = d.retrieve_data(x_param, y_param, filters, tasks, real_db(database))
    return (False, (database, tasks, params, data))


if __name__ == '__main__':
    parse_args()
    app.run(host='0.0.0.0', port=port)

